package com.i360day.redis.lock;

import com.i360day.redis.constant.RedisContextHolder;
import com.i360day.redis.exception.RedisLockException;
import org.springframework.data.redis.core.RedisTemplate;

public class RedisLock {


    private RedisTemplate redisTemplate;

    /**
     * 重试时间
     */
    private static final int DEFAULT_ACQUIRY_RETRY_MILLIS = 100;
    /**
     * 锁的后缀
     */
    private static final String LOCK_SUFFIX = "_redis_lock";
    /**
     * 锁的前缀
     **/
    private final String LOCK_PREFIX = "redis:lock:key_";
    /**
     * 锁的key
     */
    private String lockKey;
    /**
     * 锁超时时间，防止线程在入锁以后，防止阻塞后面的线程无法获取锁
     */
    private int expireMsecs = 60 * 1000;
    /**
     * 线程获取锁的等待时间
     */
    private int timeoutMsecs = 10 * 1000;
    /**
     * 是否锁定标志
     */
    private volatile boolean locked = false;

    /**
     * <p> @Description:  </p>
     *
     * <p> @Author: liju.z </p>
     *
     * <p> @Date:   2019/3/25 0025 17:12 </p>
     *
     * <p> @param redisTemplate   </p>
     * <p> @param lockKey  锁的key </p>
     *
     * <p> @return:    </p>
     **/
    public RedisLock(String lockKey) {
        this.redisTemplate = RedisContextHolder.getRedisTempate();
        this.lockKey = LOCK_PREFIX + lockKey + LOCK_SUFFIX;
    }

    /**
     * <p> @Description:  </p>
     * w
     * <p> @Author: liju.z </p>
     *
     * <p> @Date:   2019/3/25 0025 17:11 </p>
     *
     * <p> @param redisTemplate   </p>
     * <p> @param lockKey  锁的key </p>
     * <p> @param timeoutMsecs  获取锁的超时时间 </p>
     *
     * <p> @return:    </p>
     **/
    public RedisLock(String lockKey, int timeoutMsecs) {
        this(lockKey);
        this.timeoutMsecs = timeoutMsecs;
    }

    /**
     * w
     * <p> @Description:   </p>
     *
     * <p> @Author: liju.z </p>
     *
     * <p> @Date:   2019/3/25 0025 17:11 </p>
     *
     * <p> @param redisTemplate   </p>
     * <p> @param lockKey  锁的key </p>
     * <p> @param timeoutMsecs  获取锁的超时时间 </p>
     * <p> @param expireMsecs  锁的有效期 </p>
     *
     * <p> @return:    </p>
     **/
    public RedisLock(String lockKey, int timeoutMsecs, int expireMsecs) {
        this(lockKey, timeoutMsecs);
        this.expireMsecs = expireMsecs;
    }

    public static RedisLock instance(String lockKey) {
        return new RedisLock(lockKey);
    }

    public static RedisLock instance(String lockKey, int timeoutMsecs) {
        return new RedisLock(lockKey, timeoutMsecs);
    }

    public static RedisLock instance(String lockKey, int timeoutMsecs, int expireMsecs) {
        return new RedisLock(lockKey, timeoutMsecs, expireMsecs);
    }

    public String getLockKey() {
        return lockKey;
    }

    /**
     * <p> @Description: 获取缓存 </p>
     *
     * <p> @Author: liju.z </p>
     *
     * <p> @Date:   2019/3/25 0025 17:10 </p>
     *
     * <p> @param key   </p>
     *
     * <p> @return: java.lang.String   </p>
     **/
    private String get(final String key) {
        Object obj = redisTemplate.opsForValue().get(key);
        return obj != null ? obj.toString() : null;
    }

    /**
     * <p> @Description: 设置缓存 </p>
     *
     * <p> @Author: liju.z </p>
     *
     * <p> @Date:   2019/3/25 0025 17:05 </p>
     *
     * <p> @param key   </p>
     * <p> @param value   </p>
     *
     * <p> @return: boolean   </p>
     **/
    private boolean setNX(final String key, final String value) {
        return redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
     * <p> @Description: 通过key获取当前缓存缓存时间 </p>
     *
     * <p> @Author: liju.z </p>
     *
     * <p> @Date:   2019/3/25 0025 17:04 </p>
     *
     * <p> @param key   </p>
     * <p> @param value   </p>
     *
     * <p> @return: java.lang.String   </p>
     **/
    private String getSet(final String key, final String value) {
        Object obj = redisTemplate.opsForValue().getAndSet(key, value);
        return obj != null ? (String) obj : null;
    }

    /**
     * <p> @Description: 获取锁 </p>
     *
     * <p> @Author: liju.z </p>
     *
     * <p> @Date:   2019/3/25 0025 17:04 </p>
     *
     * <p> @return 获取锁成功返回ture，超时返回false   </p>
     **/
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            //锁到期时间
            String expiresStr = String.valueOf(expires);
            if (this.setNX(lockKey, expiresStr)) {
                locked = true;
                return true;
            }
            //redis里key的时间
            String currentValue = this.get(lockKey);
            //判断锁是否已经过期，过期则重新设置并获取
            if (currentValue != null && Long.parseLong(currentValue) < System.currentTimeMillis()) {
                //设置锁并返回旧值
                String oldValue = this.getSet(lockKey, expiresStr);
                //比较锁的时间，如果不一致则可能是其他锁已经修改了值并获取
                if (oldValue != null && oldValue.equals(currentValue)) {
                    locked = true;
                    return true;
                }
            }
            timeout -= DEFAULT_ACQUIRY_RETRY_MILLIS;
            //延时
            Thread.sleep(DEFAULT_ACQUIRY_RETRY_MILLIS);
        }
        return false;
    }

    /**
     * <p> @Description: 释放获取到的锁 </p>
     *
     * <p> @Author: liju.z </p>
     *
     * <p> @Date:   2019/3/25 0025 17:02 </p>
     *
     * <p> @param    </p>
     *
     * <p> @return: void   </p>
     **/
    public synchronized void unlock() {
        if (locked) {
            redisTemplate.delete(lockKey);
            locked = false;
        }
    }

    /**
     * <p> @Author liju.z <p>
     *
     * <p> @Description 锁回调，自动加锁，自动释放锁 <p>
     *
     * <p> @Date  20:08 <p>
     *
     * <p> @Param [redisLockCallback] <p>
     *
     * <p> @return [redisLockCallback] <p>
     **/
    public <T> T call(RedisLockCallback<T> redisLockCallback) throws Throwable {
        try {
            if (lock()) {
                return redisLockCallback.execute();
            }
            throw new RedisLockException();
        } catch (Throwable ex) {
            throw ex;
        } finally {
            unlock();
        }
    }

    /**
     * <p> @Author liju.z <p>
     *
     * <p> @Description 锁回调，自动加锁，自动释放锁 <p>
     *
     * <p> @Date  20:08 <p>
     *
     * <p> @Param [redisLockCallback] <p>
     *
     * <p> @return [redisLockCallback] <p>
     **/
    public void call(RedisLockNotCallback redisLockCallback) throws Throwable {
        try {
            if (lock()) {
                redisLockCallback.execute();
                return;
            }
            throw new RedisLockException();
        } catch (Throwable ex) {
            throw ex;
        } finally {
            unlock();
        }
    }
}
